Swift - Mirror反射

我们经常能听到映射和反射,那么它们代表的含义是什么呢?

映射只是一种概念,通常是用代码方式来表示一个比较复杂的意义。例如访问网站时 404 错误的含义是“找不到当前页”,500错误是“服务器内部错误”。那么我们会把这些错误的原因和一些固定的数字对应起来,这就是一种映射。

体现在程序里,映射用的比较多的地方就是和数据库打交道。我们通常会期望不直接操作数据库而对数据库做出一些改变。那么我们会声明一个类去对应数据库的一张数据表。

例如有一张学生表,有学号和姓名两个字段。我们可以在代码里建一个学生类,下面有学号和姓名两个属性,通过一定的技术可以把这个类和那张学生表对应起来,可以实现在操作这个类的时候,却去改变那张表的数据的效果,我们可以称这是一种映射。

熟悉 C#、Java 的朋友应该不难理解反射的,反射是一种技术,很多高级语言都有这种技术。具体作用就是通过一个独立存在的对象,可以找到该对象的其他信息。

例如该对象是由哪个类实例化而成的,例如有一条狗和一个人,我们通过反射技术,可以得知狗是犬科,人是灵长目的。犬科和灵长目就是我们通过反射所得到的信息,这两个词语不是一个层面的东西。

用代码来说,映射就是 a -> b,反射就是 a.ida.func() 等。

在使用 Objective-C 开发时很少强调其反射概念,因为 Objective-CRuntime 要比其他语言中的反射强大的多。在 Objective-C 中可以很简单的实现字符串和类型的转换 NSClassFromString,实现动态方法调用 performSelector: withObject:,动态赋值 KVC 等等。

这些功能大家已经习以为常,但是在其他语言中要实现这些功能却要跨过较高的门槛,而且有些根本就是无法实现的。不过在 Swift 中并不提倡使用 Runtime,而是像其他语言一样使用反射 Reflect

Swift 的反射机制是基于一个叫 Mirrorstruct 来实现的。你为具体的 subject 创建一个 Mirror,然后就可以通过它查询这个对象 subject

在我们创建 Mirror 之前,我们先创建一个可以让我们当做对象来使用的简单数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Foundation
public class Store {
let storesToDisk: Bool = true
}
public class BookmarkStore: Store {
let itemCount: Int = 10
}
public struct Bookmark {
enum Group {
case Tech
case News
}
private let store = {
return BookmarkStore()
}()
let title: String?
let url: NSURL
let keywords: [String]
let group: Group
}
let aBookmark = Bookmark(title: "Appventure", url: NSURL(string: "appventure.me")!, keywords: ["Swift", "iOS", "OSX"], group: .Tech)

创建一个 Mirror

创建 Mirror 最简单的方式就是使用 reflecting 构造器:

1
public init(reflecting subject: Any)

然后在 aBookmark struct 上使用它:

1
2
3
let aMirror = Mirror(reflecting: aBookmark)
print(aMirror)
// 输出 : Mirror for Bookmark

这段代码创建了 BookmarkMirror。正如你所见,对象的类型是 Any。这是 Swift 中最通用的类型。Swift 中的任何东西至少都是 Any 类型的。这样一来 mirror 就可以兼容 struct, class, enum, Tuple, Array, Dictionary, set 等。

Mirror 结构体还有另外三个构造器,然而这三个都是在你需要自定义 mirror 这种情况下使用的。我们会在接下来讨论自定义 mirror 时详细讲解这些额外的构造器。

Mirror 中都有什么?

Mirror struct 中包含几个 types 来帮助确定你想查询的信息。

第一个是 DisplayStyle enum,它会告诉你对象的类型:

1
2
3
4
5
6
7
8
9
10
public enum DisplayStyle {
case Struct
case Class
case Enum
case Tuple
case Optional
case Collection
case Dictionary
case Set
}

这些都是反射 API 的辅助类型。正如之前我们知道的,反射只要求对象是 Any 类型,而且 Swift 标准库中还有很多类型为 Any 的东西没有被列举在上面的 DisplayStyle enum 中。如果试图反射它们中间的某一个又会发生什么呢?比如 closure

1
2
let closure = { (a: Int) -> Int in return a * 2 }
let aMirror = Mirror(reflecting: closure)

在这种情况下,这里你会得到一个 mirror,但是 DisplayStylenil

也有提供给 Mirror 的子节点使用的 typealias

1
public typealias Child = (label: String?, value: Any)

所以每个 Child 都包含一个可选的 labelAny 类型的 value。为什么 labelOptional 的?如果你仔细考虑下,其实这是非常有意义的,并不是所有支持反射的数据结构都包含有名字的子节点。 struct 会以属性的名字做为 label,但是 Collection 只有下标,没有名字。Tuple 同样也可能没有给它们的条目指定名字。

接下来是 AncestorRepresentation enum

1
2
3
4
5
6
7
8
public enum AncestorRepresentation {
/// 为所有 ancestor class 生成默认 mirror。
case Generated
/// 使用最近的 ancestor 的 customMirror() 实现来给它创建一个 mirror。
case Customized(() -> Mirror)
/// 禁用所有 ancestor class 的行为。Mirror 的 superclassMirror() 返回值为 nil。
case Suppressed
}

这个 enum 用来定义被反射的对象的父类应该如何被反射。也就是说,这只应用于 class 类型的对象。默认情况下 Swift 会为每个父类生成额外的 mirror。然而,如果你需要做更复杂的操作,你可以使用 AncestorRepresentation enum 来定义父类被反射的细节。

如何使用一个 Mirror

现在我们有了给 Bookmark 类型的对象 aBookmark 做反射的实例变量 aMirror。可以用它来做什么呢?

下面列举了 Mirror 可用的属性 / 方法:

  • let children: Children:对象的子节点。
  • displayStyle: Mirror.DisplayStyle?:对象的展示风格
  • let subjectType: Any.Type:对象的类型
  • func superclassMirror() -> Mirror?:对象父类的 mirror

下面我们会分别对它们进行解析。

DisplayStyle

很简单,它会返回 DisplayStyle enum 的其中一种情况。如果你想要对某种不支持的类型进行反射,你会得到一个空的 Optional 值(这个之前解释过)。

1
2
print (aMirror.displayStyle)
// 此处输出:Optional(Struct)

Children

这会返回一个包含了对象所有的子节点的 AnyForwardCollection<Child>。这些子节点不单单限于 Array 或者 Dictionary 中的条目。诸如 struct 或者 class 中所有的属性也是由 AnyForwardCollection<Child> 这个属性返回的子节点。AnyForwardCollection 协议意味着这是一个支持遍历的 Collection 类型。

1
2
3
4
5
6
7
8
9
for case let (label?, value) in aMirror.children {
print (label, value)
}
//输出:
//: store main.BookmarkStore
//: title Optional("Appventure")
//: url appventure.me
//: keywords ["Swift", "iOS", "OSX"]
//: group Tech

SubjectType

这是对象的类型:

1
2
3
4
5
6
7
8
print(aMirror.subjectType)
//输出 : Bookmark
print(Mirror(reflecting: 5).subjectType)
//输出 : Int
print(Mirror(reflecting: "test").subjectType)
//输出 : String
print(Mirror(reflecting: NSNull()).subjectType)
//输出 : NSNull

然而,Swift 的文档中有下面一句话:

“当 self 是另外一个 mirrorsuperclassMirror() 时,这个类型和对象的动态类型可能会不一样。”

SuperclassMirror

这是我们对象父类的 mirror。如果这个对象不是一个类,它会是一个空的 Optional 值。如果对象的类型是基于类的,你会得到一个新的 Mirror

1
2
3
4
5
6
// 试试 struct
print(Mirror(reflecting: aBookmark).superclassMirror())
// 输出: nil
// 试试 class
print(Mirror(reflecting: aBookmark.store).superclassMirror())
// 输出: Optional(Mirror for Store)

Struct 转 Core Data

假设我们在一个叫 Books Bunny 的新兴高科技公司工作,我们以浏览器插件的方式提供了一个人工智能,它可以自动分析用户访问的所有网站,然后把相关页面自动保存到书签中。

现在 Swift 已经开源,所以我们的后台服务端肯定是用 Swift 编写。因为在我们的系统中同时有数以百万计的网站访问活动,我们想用 struct 来存储用户访问网站的分析数据。不过,如果我们 AI 认定某个页面的数据是需要保存到书签中的话,我们需要使用 CoreData 来把这个类型的对象保存到数据库中。

现在我们不想为每个新建的 struct 单独写自定义的 Core Data 序列化代码。而是想以一种更优雅的方式来开发,从而可以让将来的所有 struct 都可以利用这种方式来做序列化。

那么我们该怎么做呢?

一个协议

记住,我们有一个 struct,它需要自动转换为 NSManagedObjectCore Data)。

如果我们想要支持不同的 struct 甚至类型,我们可以用协议来实现,然后确保我们需要的类型符合这个协议。所以我们假想的协议应该有哪些功能呢?

  • 第一,协议应该允许自定义我们想要创建的 Core Data 实体的名字
  • 第二,协议需要提供一种方式来告诉它如何转换为 NSManagedObject

我们的 protocol(协议) 看起来是下面这个样子的:

1
2
3
4
5
6
protocol StructDecoder {
// 我们 Core Data 实体的名字
static var EntityName: String { get }
// 返回包含我们属性集的 NSManagedObject
func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject
}

toCoreData 方法使用了 Swift 2.0 新的异常处理来抛出错误,如果转换失败,会有几种错误情况,这些情况都在下面的 ErrorTypeenum 进行了列举:

1
2
3
4
5
6
7
8
enum SerializationError: ErrorType {
// 我们只支持 struct
case StructRequired
// 实体在 Core Data 模型中不存在
case UnknownEntity(name: String)
// 给定的类型不能保存在 core data 中
case UnsupportedSubType(label: String?)
}

上面列举了三种转换时需要注意的错误情况。第一种情况是我们试图把它应用到非 struct 的对象上。第二种情况是我们想要创建的 entity 在 Core Data 模型中不存在。第三种情况是我们想要把一些不能存储在 Core Data 中的东西保存到 Core Data 中(即 enum)。

让我们创建一个 struct 然后为其增加协议一致性:

Bookmark struct

1
2
3
4
5
6
struct Bookmark {
let title: String
let url: NSURL
let pagerank: Int
let created: NSDate
}

接下来,我们要实现 toCoreData 方法。

协议扩展

当然我们可以为每个 struct 都写新的 toCoreData 方法,但是工作量很大,因为 struct 不支持继承,所以我们不能使用基类的方式。不过我们可以使用 protocol extension 来扩展这个方法到所有相符合的 struct

1
2
3
4
extension StructDecoder {
func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject {
}
}

因为扩展已经被应用到相符合的 struct,这个方法就可以在 struct 的上下文中被调用。因此,在协议中,self 指的是我们想分析的 struct

所以,我们需要做的第一步就是创建一个可以写入我们 Bookmark struct 值的NSManagedObject。我们该怎么做呢?

一点 Core Data

Core Data 有点啰嗦,所以如果需要创建一个对象,我们需要如下的步骤:

  1. 获得我们需要创建的实体的名字(字符串)
  2. 获取 NSManagedObjectContext,然后为我们的实体创建 NSEntityDescription
  3. 利用这些信息创建 NSManagedObject

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 获取 Core Data 实体的名字
let entityName = self.dynamicType.EntityName
// 创建实体描述
// 实体可能不存在, 所以我们使用 'guard let' 来判断,如果实体
// 在我们的 core data 模型中不存在的话,我们就抛出错误
guard let desc = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
else { throw UnknownEntity(name: entityName) }
// 创建 NSManagedObject
let managedObject = NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)

实现反射

下一步,我们想使用反射 API 来读取 bookmark 对象的属性然后把它写入到 NSManagedObject 实例中。

1
2
3
4
5
// 创建 Mirror
let mirror = Mirror(reflecting: self)
// 确保我们是在分析一个 struct
guard mirror.displayStyle == .Struct else { throw SerializationError.StructRequired }

我们通过测试 displayStyle 属性的方式来确保这是一个 struct

所以现在我们有了一个可以让我们读取属性的 Mirror,也有了一个可以用来设置属性的 NSManagedObject。因为 mirror 提供了读取所有 children 的方式,所以我们可以遍历它们并保存它们的值。方式如下:

1
2
3
for case let (label?, value) in mirror.children {
managedObject.setValue(value, forKey: label)
}

太棒了!但是,如果我们试图编译它,它会失败。原因是 setValueForKey 需要一个 AnyObject? 类型的对象,而我们的 children 属性只返回一个 (String?, Any) 类型的 tuple。也就是说 valueAny 类型但是我们需要 AnyObject 类型的。为了解决这个问题,我们要测试 valueAnyObject 协议一致性。这也意味着如果得到的属性的类型不符合 AnyObject 协议(比如 enum),我们就可以抛出一个错误。

1
2
3
4
5
6
7
8
9
10
11
12
let mirror = Mirror(reflecting: self)
guard mirror.displayStyle == .Struct
else { throw SerializationError.StructRequired }
for case let (label?, anyValue) in mirror.children {
if let value = anyValue as? AnyObject {
managedObject.setValue(value, forKey: label)
} else {
throw SerializationError.UnsupportedSubType(label: label)
}
}

现在,只有在 childAnyObject 类型的时候我们才会调用 setValueForKey 方法。

然后唯一剩下的事情就是返回 NSManagedObject。完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
extension StructDecoder {
func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject {
let entityName = self.dynamicType.EntityName
// 创建实体描述
guard let desc = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
else { throw UnknownEntity(name: entityName) } // [译者注] UnknownEntity 为 SerializationError.UnknownEntity
// 创建 NSManagedObject
let managedObject = NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)
// 创建一个 Mirror
let mirror = Mirror(reflecting: self)
// 确保我们是在分析一个 struct
guard mirror.displayStyle == .Struct else { throw SerializationError.StructRequired }
for case let (label?, anyValue) in mirror.children {
if let value = anyValue as? AnyObject {
managedObject.setValue(child, forKey: label) // [译者注] 正确代码为:managedObject.setValue(value, forKey: label)
} else {
throw SerializationError.UnsupportedSubType(label: label)
}
}
return managedObject
}
}

搞定,我们现在已经把 struct 转换为 NSManagedObject 了。

Class 转 Dictionary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//自定义用户类
class User {
var name:String = "" //姓名
var nickname:String? //昵称
var age:Int? //年龄
var emails:[String]? //邮件地址
var tels:[Telephone]? //电话
}
//电话结构体
struct Telephone {
var title:String //电话标题
var number:String //电话号码
}
//自定义一个JSON协议
protocol JSON {
func toJSONModel() -> Any?
}
//扩展协议方法,实现一个通用的toJSONModel方法(反射实现)
extension JSON {
//将模型数据转成可用的字典数据,Any表示任何类型,除了方法类型
func toJSONModel() -> Any? {
//根据实例创建反射结构体Mirror
let mirror = Mirror(reflecting: self)
if mirror.children.count > 0 {
//创建一个空字典,用于后面添加键值对
var result: [String:Any] = [:]
//遍历实例的所有属性集合
for children in mirror.children {
let propertyNameString = children.label!
let value = children.value
//判断value的类型是否遵循JSON协议,进行深度递归调用
if let jsonValue = value as? JSON {
result[propertyNameString] = jsonValue.toJSONModel()
}
}
return result
}
return self
}
}
//扩展可选类型,使其遵循JSON协议,可选类型值为nil时,不转化进字典中
extension Optional: JSON {
//可选类型重写toJSONModel()方法
func toJSONModel() -> Any? {
if let x = self {
if let value = x as? JSON {
return value.toJSONModel()
}
}
return nil
}
}
//扩展两个自定义类型,使其遵循JSON协议
extension User: JSON { }
extension Telephone: JSON { }
//扩展Swift的基本数据类型,使其遵循JSON协议
extension String: JSON { }
extension Int: JSON { }
extension Bool: JSON { }
extension Dictionary: JSON { }
extension Array: JSON { }
//创建一个User实例对象模型
let user1 = User()
user1.name = "hangge"
user1.age = 100
user1.emails = ["hangge@hangge.com","system@hangge.com"]
//添加电话
let tel1 = Telephone(title: "手机", number: "123456")
let tel2 = Telephone(title: "公司座机", number: "001-0358")
user1.tels = [tel1, tel2]
//模型转字典
if let model = user1.toJSONModel() {
print(model)
}
/* 打印:【以下打印经过排版,正式的打印是紧凑的】
[
"tels": [
"[1]": [
"title": "公司座机",
"number": "001-0358"
],
"[0]": [
"title": "手机",
"number": "123456"
]
],
"name": "hangge",
"emails": [
"[1]": "system@hangge.com",
"[0]": "hangge@hangge.com"
],
"age": 100
]
*/

自定义 Mirror

我们之前已经讨论过,创建 Mirror 还有其他的选项。这些选项是非常有用的,比如,你想自己定义 mirror对象的哪些部分是可访问的。对于这种情况 Mirror Struct 提供了其他的构造器。

Collection

第一个特殊 init 是为 Collection 量身定做的:

1
2
3
4
public init<T, C : CollectionType where C.Generator.Element == Child>
(_ subject: T, children: C,
displayStyle: Mirror.DisplayStyle? = default,
ancestorRepresentation: Mirror.AncestorRepresentation = default)

与之前的 init(reflecting:) 相比,这个构造器允许我们定义更多反射处理的细节。

  • 它只对 Collection 有效
  • 我们可以设定被反射的对象以及对象的 childrenCollection 的内容)

Class 或者 Struct

第二个可以在 class 或者 struct 上使用。

1
2
3
4
public init<T>(_ subject: T,
children: DictionaryLiteral<String, Any>,
displayStyle: Mirror.DisplayStyle? = default,
ancestorRepresentation: Mirror.AncestorRepresentation = default)

有意思的是,这里是由你指定对象的 children (即属性),指定的方式是通过一个 DictionaryLiteral,它有点像字典,可以直接用作函数参数。如果我们为 Bookmark struct 实现这个构造器,它看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
extension Bookmark: CustomReflectable {
func customMirror() -> Mirror { // [译者注] 此处应该为 public func customMirror() -> Mirror {
let children = DictionaryLiteral<String, Any>(dictionaryLiteral:
("title", self.title), ("pagerank", self.pagerank),
("url", self.url), ("created", self.created),
("keywords", self.keywords), ("group", self.group))
return Mirror.init(Bookmark.self, children: children,
displayStyle: Mirror.DisplayStyle.Struct,
ancestorRepresentation:.Suppressed)
}
}

用例

所以留下来让我们思考的问题是什么呢?好的反射用例又是什么呢?很显然,如果你在很多 NSManagedObject 上使用反射,它会大大降低你代码的性能。同时如果只有一个或者两个 struct,根据自己掌握的struct 领域的知识编写一个序列化的方法会更容易,更高性能且更不容易让人困惑。

而本文展示反射技巧可以当你在有很多复杂的 struct ,且偶尔想对它们中的一部分进行存储时使用。

例子如下:

  • 设置收藏夹
  • 收藏书签
  • 加星
  • 记住上一次选择
  • 在重新启动时存储 AST 打开的项目
  • 在特殊处理时做临时存储

当然除此之外,反射当然还有其他的使用场景:

  • 遍历 tuples
  • 对类做分析
  • 运行时分析对象的一致性
  • 自动生成详细日志 / 调试信息(即外部生成对象)

参考链接

Swift 反射 API 及用法:

http://swift.gg/2015/11/23/swift-reflection-api-what-you-can-do/